#!/usr/bin/env python
# -*- coding:utf-8 -*-

'''
TODO :
    errorback should be added for producer, a error-handle for synchronous rpc.
'''

'''
PACKET SPECIFICATION:
    packet ::= route_slice separator data_slice
    separator ::= '\t'

    route_slice         ::= action_field service_field id_field

    action_field        ::= 'action:' action_name
    action_name         ::= 'REQUEST' | 'RESPONSE' | 'FORWARD'

    service_field       ::= 'service:' service_name
    service_name        ::= letter (letter | digit)*

    id_field            ::= packet_id_field [respone_id_field]
    packet_id_field     ::= 'packid:' decimalinteger
    response_id_field   ::= 'respid:' decimalinteger

    data_slice ::= 'body:' data
    data       ::= base64_encode_string

    identifier ::=  (letter|"_") (letter | digit | "_")*
    letter     ::=  lowercase | uppercase
    lowercase  ::=  "a"..."z"
    uppercase  ::=  "A"..."Z"
    digit      ::=  "0"..."9"

    decimalinteger ::=  nonzerodigit digit* | "0"
    nonzerodigit   ::=  "1"..."9"

'''

import os, sys, signal
import asyncore, asynchat, socket, threading, time
import base64


will_stop = False

def signal_stop( signo, frame ) :
    global  will_stop
    #DEBUG( "JUJUB catch #%d signal, would stop.", signo )
    will_stop = True
    return
    # signal_stop


#######################################################################

TERMINATOR = '\n'

MIN_PACKET_ID = 1
MAX_PACKET_ID = 65535
packet_id_current = MAX_PACKET_ID

PP_ACTION_REQUEST  = 'REQUEST'
PP_ACTION_RESPONSE = 'RESPONSE'


class   Packet(object)  :
    def __init__(self) :
        self.action = None
        self.service = None
        self.body = None
        self.packid = 0
        self.respid = 0
        # __init__()

    def __str__(self) :
        s = "action:%s service:%s packid:%d" % (self.action, self.service, self.packid)
        if self.respid > 0 :
            s += " respid:%d" % self.respid
        s += " body:'%s'" % self.body
        return s
        # __str__()

    def isComplete(self) :
        if not self.action or not self.service or not self.body :
            return  False
        return  True

    def setRequest(self) :
        if PP_ACTION_REQUEST == self.action : return
        self.action = PP_ACTION_REQUEST
    def setResponse(self) :
        if PP_ACTION_RESPONSE == self.action : return
        self.action = PP_ACTION_RESPONSE

    def isRequest(self) :
        if PP_ACTION_REQUEST == self.action : return True
        return  False
    def isResponse(self) :
        if PP_ACTION_RESPONSE == self.action : return True
        return  False
    # class Packet

def depack(record) :
    '''decode packet record string into Packet struct'''
    field_separator = '\t'
    kv_separator = ':'
    packet = Packet()
    fields = record.split(field_separator)
    for f in fields :
        (key, value) = f.split(kv_separator, 1)
        if not value : continue
        if 'action' == key :
            packet.action = value
        elif 'service' == key :
            packet.service = value
        elif 'packid' == key :
            packet.packid = int(value)
        elif 'respid' == key :
            packet.respid = int(value)
        elif 'body' == key :
            packet.body = base64.b64decode(value)

    if not packet.isComplete() : return None
    return  packet
    # depack()

def enpack(packet) :
    '''encode Packet struct into packet record string'''
    packid = packet.packid
    if not packid :  packid = nextPacketID()
    record = "action:%s\tservice:%s\tpackid:%d" % (packet.action, packet.service, packid)
    if packet.respid > 0 :
        record += "\trespid:%d" % packet.respid
    record += "\tbody:%s" % base64.b64encode(packet.body)
    return  (record, packid)
    # enpack

def nextPacketID() :
    global  packet_id_current
    if packet_id_current == MAX_PACKET_ID :
        packet_id_current = MIN_PACKET_ID
    else :
        packet_id_current += 1
    return  packet_id_current
    # packetid()

#######################################################################

class   Context(object) :
    '''Context -- context for Consumer'''
    def __init__(self, dispatcher) :
        self.dispatcher = dispatcher
        # __init__()
    # class Context

#######################################################################

class   Consumer(object) :
    def response(self, packet, channel) :
        packet.setResponse()
        msg,packid = enpack(packet)
        channel.push(msg + '\n')
        # response()

    def feed(self, packet, channel_from, dispatcher) :
        '''return True on success, False on failure'''
        raise NotImplementedError
        # feed()
    # class Consumer()

class   Producer(object) :
    def empty(self) :
        '''if the producer may be gone, return True; otherwise, return False'''
        return  True
        # empty()

    def more(self) :
        '''return a complete application record once a time. if no record available temporarily, return None'''
        raise NotImplementedError
        # more()
    # class Producer


class   Dispatcher(object)  :
    '''Dispatcher -- dispatch message to consumer'''
    def __init__(self) :
        # table-address : address --> channel
        self.__table_address = {}
        # table-channel : channel --> state of channel
        self.__table_channel = {}
        # table-consumer : SERVICE --> consumer
        self.__table_consumer = {}
        # __init__()

    def forward(self, channel_from, record) :
        '''forward a record to a channel
        '''
        pass
        # forward()

    def dispatch(self, channel_from, record) :
        '''dispatch a record to a consumer

        List of Actions
         * parse record to get address info (such as SERVICE name)
         * lookup route-table to get the Consumer who is responsible for the record,
            if No Consumer is responsible for it :
                discard the message(record)
                return False
            OR
            if No Consumer is responsible for it :
                use Router as Consumer (route the message again)
         * feed message to consumer
         * return True
        '''
        # parse record, to get SERVICE
        packet = depack(record)
        if not packet : raise RuntimeError, 'invalid record: %s' % record

        if packet.isRequest() :
            service = packet.service
            # find Consumer for the SERVICE
            if service not in self.__table_consumer : return  False
            #    for each SERVICE, organize Consumers as a list, we just use the first consumer
            consumer = self.__table_consumer[service][0]
            # just ignore, or log it to some where if NOT ok
            ok = consumer.feed(packet, channel_from, self)
        elif packet.isResponse() :
            if channel_from not in self.__table_channel : return True
            service, producer, consumer, queue_out, queue_err = self.__table_channel[channel_from][0]
            if consumer is None : return True
            if service != packet.service : return False
            ok = consumer.feed(packet, channel_from, self)
            # for RESPONSE consumer
            if packet.respid in queue_out :
                if ok :
                    # erase packet from sending queue
                    del queue_out[ packet.respid ]
                else :
                    # move packet from sending-queue to error-queue
                    queue_err[ packet.respid ] = queue_out[ packet.respid ]
                    del queue_out[ packet.respid ]
            else :
                # suspicious packet response id
                pass
        else :
            raise RuntimeError, 'cannot handle %s action' % packet.action

        return  True
        # dispatch()

    def registerConsumer(self, service, consumer) :
        if not service : raise RuntimeError, 'Dispatcher: invalid service to register'
        if not consumer : raise RuntimeError, 'Dispatcher: invalid consumer to register'
        if service in self.__table_consumer :
            self.__table_consumer[service].append(consumer)
        else :
            self.__table_consumer[service] = [consumer,]
        # registerConsumer()
    def unregisterConsumer(self, service, consumer) :
        if not service : raise RuntimeError, 'Dispatcher: invalid service to register'
        if not consumer : raise RuntimeError, 'Dispatcher: invalid consumer to register'
        if service in self.__table_consumer :
            self.__table_consumer[service].remove(consumer)
        # unregisterConsumer()

    def registerChannel(self, address, channel) :
        if not address or not channel : return
        self.__table_address[ address ] = channel
        # registerChannel()
    def unregisterChannel(self, address) :
        if not address : return
        if address in self.__table_address :
            channel = self.__table_address[ address ]
            if channel :
                self.unregisterProducer( channel, None )
            del self.__table_address[ address ]
        # unregisterChannel()
    def getChannelByAddress(self, address) :
        if not address : return None
        if address in self.__table_address :
            return self.__table_address[ address ]
        return None
        # getChannelByAddress()

    def registerProducer(self, channel, service, producer, consumer=None) :
        '''register a (producer, consumer) for serivce of channel'''
        queue_out = {}
        queue_err = {}
        if channel in self.__table_channel :
            self.__table_channel[ channel ].append( (service, producer, consumer, queue_out, queue_err) )
        else :
            self.__table_channel[ channel ] = [ (service, producer, consumer, queue_out, queue_err), ]
        # registerProducer()

    def unregisterProducer(self, channel, service, producer=None, consumer=None) :
        '''unregister a (producer, consumer) for service of channel'''
        if channel not in self.__table_channel : return
        if service is None :
            # unregister ALL service for channel
            del self.__table_channel[ channel ]
            return

        producers = self.__table_channel[ channel ]
        index_to_delete = []
        for idx in range(len(producers)) :
            s,p,c,q_out, q_err = producers[idx]
            if s != service : continue
            if producer is None :
                index_to_delete.append(idx)
                continue
            else :
                if p != producer : continue
                if consumer is None :
                    index_to_delete.append(idx)
                    continue
                else :
                    if s != consumer : continue
                    index_to_delete.append(idx)
        while len(index_to_delete) > 0 :
            idx = index_to_delete.pop()
            del producers[idx]

        if len(producers) < 1 :
            del self.__table_channel[ channel ]
        # unregisterProducer()

    def countAliveProducers(self) :
        count_alive = 0
        for channel, producers in self.__table_channel.iteritems() :
            # if channel has sent out all data
            if len(channel.producer_fifo) > 0 :
                # there are something to sent
                count_alive += 1
                continue
            if len(producers) > 0 :
                count_alive += 1
                continue
        return  count_alive
        # countAliveProducers()

    def loop_once(self) :
        '''loop_once : do some routines'''
        channel_to_delete = []
        for channel, producers in self.__table_channel.iteritems() :
            # if channel has sent out all data
            if len(channel.producer_fifo) > 0 :
                # there are something to sent
                continue
            # make producer to produce new data
            if len(producers) < 1 :
                channel_to_delete.append( channel )
                continue
            service, producer, consumer, q_out, q_err = producers[0]
            if len(q_out) > 0 :
                # there are some request waiting response
                continue
            if len(q_err) > 0 :
                # there are some ERROR request needing to handle
                continue
            if producer is None :
                # no producer, who produce data???
                continue
            if producer.empty() :
                # producer exhaust
                channel_to_delete.append( channel )
                continue
            record = producer.more()
            if record is None :
                continue
            # make a new packet
            packet = Packet()
            packet.setRequest()
            packet.service = service
            packet.body = record
            msg, packid = enpack( packet )
            packet.packid = packid
            channel.push( msg + TERMINATOR )
            q_out[packid] = packet

        for chnl in channel_to_delete :
            del self.__table_channel[ chnl ]
        # loop_once()

    # class Dispatcher

#######################################################################

class   JuChannel(asynchat.async_chat) :
    def    __init__(self, address, context, connect=None) :
        asynchat.async_chat.__init__(self, conn=connect)
        if connect :
            self.__connect = connect
        else :
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            self.connect(address)
            self.__connect = self.socket
        self.address = address
        self.__buf_incoming = []
        self.__terminator = TERMINATOR
        self.set_terminator(self.__terminator)

        self.__context = context
        self.__context.dispatcher.registerChannel(self.address, self)
        # __init__()

    def    collect_incoming_data(self, data) :
        #DEBUG('JUJUB collect: >>>%s<<<', data)
        self.__buf_incoming.append(data)
        # collect_incoming_data()

    def    found_terminator(self) :
        #DEBUG('JUJUB found: >>>%s<<<', self.__buf_incoming)

        line = ''.join(self.__buf_incoming)
        self.__buf_incoming = []

        self.__context.dispatcher.dispatch(self, line)
        # found_terminator()

    def    handle_connect(self) :
        pass
        # handle_connect()

    def    handle_close(self) :
        #DEBUG('JUJUB close connection for client "%s".', self.address)
        self.__context.dispatcher.unregisterChannel(self.address)
        self.close()
        # handle_close()

    # class JuChannel

class   JuServer(asyncore.dispatcher):

    def __init__(self, port, context):
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.bind(("", port))
        self.listen(5)
        #DEBUG('JUJUB listening on PORT:%d ...', port)
        self.context = context
        # __init__()

    def handle_accept(self) :
        connect, client_address = self.accept()
        #DEBUG('JUJUB accept client %s', client)

        JuChannel(client_address, self.context, connect)
        # handle_accept()
    # class JuServer


#######################################################################

class   Jujube(object) :
    def __init__(self) :
        dispatcher = Dispatcher()
        context = Context(dispatcher)
        self.__context = context
        self.__listeners = {}
        self.__transporters = {}
        # __init__()

    def listen(self, port) :
        if port in self.__listeners : return
        listener = JuServer(port, self.__context)
        self.__listeners[port] = listener
        # listen()

    def registerConsumer(self, service, consumer) :
        '''register REQUEST consumer'''
        self.__context.dispatcher.registerConsumer(service, consumer)
        # registerConsumer()

    def unregisterConsumer(self, service, consumer) :
        '''unregister REQUEST consumer'''
        self.__context.dispatcher.unregisterConsumer(service, consumer)
        # unregisterConsumer()

    def run(self) :
        # mask signal
        #DEBUG('os.name: %s', os.name)
        signal.signal( signal.SIGTERM, signal_stop )
        if 'posix' == os.name :
            signal.signal( signal.SIGQUIT, signal_stop )

        try :
            while True :
                if len(asyncore.socket_map) < 1 : break
                asyncore.loop(timeout=10.0, count=100)
        except :
            #DEBUG( "JUJUB interrupt : %s", `sys.exc_info()[1]` )
            pass
        # run()

    def connect(self, address) :
        JuChannel(address, self.__context)
        # connect()

    def registerProducer(self, address, service, producer, consumer=None) :
        channel = self.__context.dispatcher.getChannelByAddress(address)
        if channel is None :
            raise RuntimeError, 'No registered channel for address %s' % address
        self.__context.dispatcher.registerProducer(channel, service, producer, consumer)
        # registerProducer()
    def unregisterProducer(self, address, service, prodcuer, consumer=None) :
        channel = self.__context.dispatcher.getChannelByAddress(address)
        if channel is None :
            raise RuntimeError, 'No registered channel for address %s' % address
        self.__context.dispatcher.unregisterProducer(channel, service, producer, consumer)
        # unregisterProducer()

    def runsync(self) :
        '''run synchrously'''
        # mask signal
        #DEBUG('os.name: %s', os.name)
        signal.signal( signal.SIGTERM, signal_stop )
        if 'posix' == os.name :
            signal.signal( signal.SIGQUIT, signal_stop )

        try :
            while True :
                self.__context.dispatcher.loop_once()
                if self.__context.dispatcher.countAliveProducers() < 1 : break
                if len(asyncore.socket_map) < 1 : break
                asyncore.loop(timeout=1.0, count=1)
        except :
            #DEBUG( "JUJUB interrupt : %s", `sys.exc_info()[1]` )
            pass
        # runsync()

    # class Jujube


